Add mock generation for @Instantiable types#204
Draft
dfed wants to merge 120 commits intodfed/root-scannerfrom
Draft
Add mock generation for @Instantiable types#204dfed wants to merge 120 commits intodfed/root-scannerfrom
dfed wants to merge 120 commits intodfed/root-scannerfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dfed/root-scanner #204 +/- ##
=====================================================
- Coverage 99.92% 99.89% -0.03%
=====================================================
Files 40 40
Lines 3809 4924 +1115
=====================================================
+ Hits 3806 4919 +1113
- Misses 3 5 +2
🚀 New features to boost your workflow:
|
dfed
commented
Apr 1, 2026
SafeDI now automatically generates `mock()` methods for every `@Instantiable` type, building full dependency subtrees with overridable closure parameters. New @SafeDIConfiguration properties: - `generateMocks: Bool` (default true) — controls mock generation - `mockConditionalCompilation: StaticString?` (default "DEBUG") — #if wrapping New @INSTANTIABLE parameter: - `mockAttributes: StaticString` — attributes for generated mock() (e.g. "@mainactor") Each mock gets a `SafeDIMockPath` enum with nested enums per dependency type, enabling callers to differentiate same-type dependencies at different tree paths. The build plugin now generates mock output files alongside DI tree files. Multi-module projects can add the plugin to all targets for per-module mocks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Deduplicate mock generation for types with fulfillingAdditionalTypes - Add generateMocks/mockConditionalCompilation to ExampleMultiProjectIntegration config - Fix swiftformat lint issues in MockGenerator - Scope mock generation to target files to avoid multi-module duplicates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mock generation for Instantiator<T> and erasedToConcreteExistential types is not yet supported. Disable mocks in Xcode project examples (which use these features) while the SPM package examples work correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Xcode project needs the config file in its project.pbxproj to pick up generateMocks: false. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unreachable isExtension branch from generateSimpleMock and unused defaultConstruction method. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…types - Rewrite SafeDIToolMockGenerationTests to use full output comparison (no `contains`), matching SafeDIToolCodeGenerationTests style - Add #Preview blocks using .mock() to views in both Xcode example projects - Re-enable generateMocks for Xcode example projects - MockGenerator: skip types with Instantiator deps (not yet supported) - MockGenerator: make params required (no default) for types not in type map - Track hasKnownMock per type entry for required vs optional params - Add test for extension-based type with nil conditional compilation - Add test for required parameter when type not in type map Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Types not in the type map (like AnyUserService) now get non-optional closure parameters (@escaping, no `?`) instead of optional closures with a broken default. This ensures the generated code compiles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For types instantiated with `erasedToConcreteExistential: true`, the mock now generates TWO parameters: one for the concrete type (DefaultMyService) and one for the erased wrapper (AnyMyService). The erased type's default wraps the concrete type: `AnyMyService(defaultMyService)`. Non-@INSTANTIABLE types now use non-optional @escaping closure parameters instead of optional closures, avoiding broken defaults. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… coverage - MockGenerator now detects erasedToConcreteExistential relationships globally and auto-generates wrapping for received types (e.g., AnyUserService wraps DefaultUserService.mock()) - #Preview blocks simplified to NameEntryView.mock() and NoteView.mock(userName:) - Consolidated duplicate arg-matching branches in buildInlineConstruction - Added hasReceivedDepsInScope check for sourceType form - Added test for received erased type auto-wrapping - Added test for complex mock with nil conditional compilation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For received erased types (like @received AnyUserService), the mock now has only the erased type as a parameter — the concrete type (DefaultUserService) is built inline in the default construction: AnyUserService(DefaultUserService.mock()) For @Instantiated(erasedToConcreteExistential: true) at the root level, both the concrete and erased type remain parameters, with the erased type referencing the concrete variable. #Preview blocks simplified to zero manual construction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Multiple branches receiving the same @received property - Protocol type fulfilled by fulfillingAdditionalTypes - Multiple roots each getting their own mock file - Construction ordering respects @received dependencies - Four-level deep tree with shared leaf threading 19 mock generation tests total. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ExampleMultiProjectIntegration uses additionalDirectoriesToInclude for Subproject files. The Xcode plugin only scans target.inputFiles for mock entries, so Subproject types (DefaultUserService, UserDefaults) don't get generated mocks. The single-project example keeps .mock() #Previews since all its files are in the target. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mock generation does not yet support types from additionalDirectoriesToInclude. The Xcode plugin only scans target.inputFiles for mock entries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add test for missing mockConditionalCompilation fix-it - Add test for mockConditionalCompilation without initializer - Add test for inline construction skipping default-valued arguments - Add test for RootScanner.outputFiles computed property - Remove unreachable defensive branches (force-unwrap known-good lookups) - Extract wrapInConditionalCompilation helper to fix coverage instrumentation - Simplify topological sort dependency check 413 tests, 0 uncovered new lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove hasUnsupportedDeps skip — all @INSTANTIABLE types now get mocks - TypeEntry gains enumName, paramLabel, isInstantiator, builtTypeForwardedProperties - Instantiator deps use property label as enum name - Default wraps inline tree in Instantiator { forwarded in ... } closure - Forwarded props become Instantiator closure parameters - Topological sort handles Instantiator deps (wait for captured parent vars) - No boundary — transitive deps inside Instantiator are parent mock params Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test Instantiator<T> with forwarded properties (closure param) - Test Instantiator<T> without forwarded properties - Both verify enum naming, parameter types, and inline closure construction Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When no @SafeDIConfiguration is found, generateMocks now defaults to false. Modules must explicitly opt in via @SafeDIConfiguration. Added enableMockGeneration parameter to test helper and test for the no-config default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Types included via additionalDirectoriesToInclude are not scanned for mock generation. Document this limitation in the Manual and in the ExampleMultiProjectIntegration SafeDIConfiguration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test Instantiator with multiple forwarded properties (tuple destructuring) - Remove unused parameterLabel(for:) method - Update documentation: mock generation is per-module, not per-project Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Force unwraps are not the pattern in this codebase. Restore proper guard/else fallbacks in buildInlineConstruction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extension-based type with received deps (inline .instantiate()) - Extension-based type as inline construction target - Instantiator with default-valued built type argument Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add SafeDIGenerator plugin dependency to Subproject Xcode framework target - Add @SafeDIConfiguration to Subproject with generateMocks: true - Re-enable generateMocks on main app target - Update #Preview blocks to use .mock() - Keep additionalDirectoriesToInclude for DI tree generation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All positive mock generation test assertions now use exact full-output
comparison. Only negative checks (!contains("extension")) remain for
tests verifying mocks are NOT generated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instantiator types always have hasKnownMock = true because the built type must be @INSTANTIABLE (validated upstream). The else branch was unreachable dead code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unreachable defensive branches (trust validated data, matching ScopeGenerator/DependencyTreeGenerator pattern of 0 uncovered lines) - Simplify arg building to use nil-coalescing on optional initializer - Keep extension type checks for .instantiate() calls - Add lazy self-instantiation cycle test (exercises topo sort cycle breaker) 422 tests, 0 uncovered lines in MockGenerator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Forwarded closure parameters (e.g., onDismiss: () -> Void) need @escaping in the mock function signature since they're passed to an init that stores them. Fixed by using asFunctionParameter (which adds @escaping for closure types) instead of bare asSource for forwarded declaration source types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename `casesStr` to `casesString` (no abbreviations) - Fix `dep` → `dependency` in comment - Fix `param` → `parameter` and `params` → `parameters` in comments - Rename test functions: `Dep` → `Dependency` in three test names - Convert `if continue` patterns to `guard` in ScopeGenerator Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Replace partial .contains() assertion in mock_passesNilForOnlyIfAvailableProtocolDependency with full output equality check, matching the convention of all other mock tests. 2. Fix latent bug in isOnlyIfAvailable check: use unwrapped Property identity (label + type) instead of just label when checking for Optional counterparts. This prevents false collisions when unrelated types share a label (e.g., `service: ConcreteService` aliased onlyIfAvailable coexisting with `service: ServiceProtocol?` Optional). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The boolean controls whether a mock parameter is optional (= nil) or required (@escaping). The old name suggested something about mock existence rather than parameter optionality. Added doc comment explaining the three cases: known default construction, onlyIfAvailable, and not constructible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix Manual.md: mock generation requires @SafeDIConfiguration (was incorrectly stated as enabled by default); fix path enum example to use actual generated case names (root, childA) not stale format - Fix SafeDIConfiguration.swift doc: clarify generateMocks default is true only when @SafeDIConfiguration exists - Remove duplicate doc-comment line in ScopeGenerator.swift - Improve doc comments: isOptionalParameter, onlyIfAvailable set, alias guard explanation - Rename queue → worklist and BFS → worklist in DependencyTreeGenerator (code uses popLast/stack semantics, not FIFO) - Improve memoization cache comment - Delete stale mock_audit_issues.md (all issues resolved) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move scope lookup from createMockRootScopeGenerator to caller — the caller's guard already has other conditions that cover the else branch, so no new uncoverable path is introduced - Restructure childMockCodeGeneration to accept the child's label (String?) instead of extracting it from ScopeGenerator.property via a guard. Uses flatMap for the optional label lookup. - Remove unreachable `guard !code.isEmpty` — generateMockRootCode always produces non-empty code (extension wrapper at minimum) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both cases return the same associated value. Merging them into a single pattern eliminates the uncovered .root branch (only .property is reached during mock generation, but .root is reached during production generation — merging means both cover the same line). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a child type defines a `static func mock(...)` with parameters matching its dependencies, the generated parent mock now calls `Child.mock(dependency: dependency)` instead of `Child(dependency: ...)`. This ensures user-defined mock behavior (e.g., preview setup, test configuration) is preserved in the dependency tree. - Replace `hasExistingMockMethod: Bool` with `mockInitializer: Initializer?` on Instantiable, parsing the mock function signature via the existing `Initializer(FunctionDeclSyntax)` constructor - In mock code generation, use `.mock()` when the mock initializer has parameters (no-parameter mocks can't thread dependencies and fall back to the regular initializer) - Types with user-defined mocks still skip mock FILE generation (no collision with the user's method) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…gument Store the actual default value expression text (e.g., "nil", ".init()") instead of just a boolean. hasDefaultValue becomes a computed property derived from defaultValueExpression != nil. This preserves the default value source text needed for bubbling default-valued parameters up to mock signatures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Types with user-defined mock methods are skipped in generateMockCode (DependencyTreeGenerator guard), so the root-level construction code never sees a mockInitializer. The .mock() construction only applies at the child level (in generatePropertyCode). Removed the dead branches from both simple-mock and complex-mock root paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change 1 completion: - Add macro validation: mock() must be public (with fix-it to add modifier). Argument validation for dependencies requires the full tool pipeline (macro expansion strips decorators before visitor runs). - Fix extension-based types: mockInitializer was nil when instantiate() appeared before mock() in source order. The instantiables getter now patches mockInitializer onto extension instantiables after all visits. - Add tests: deep tree with user mock, extension-based type with user mock, reversed source order for extension, non-public mock diagnostic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mock_existingMockMethodWithMultipleDependencies: verifies all dependency parameters are threaded to Child.mock() - mock_existingMockMethodSkipsGenerationForTypeButGeneratesForParent: verifies child gets no generated mock while parent gets SafeDIMockPath enum and calls Child.mock() — uses full output equality assertions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n, var mockInitializer - Extract buildFixedParameterClause helper in InstantiableMacro to deduplicate parameter-list fix-it logic between init and mock validation - Make instantiationDeclaration conditional on codeGeneration mode (avoids computing mock declaration string in dependency tree mode) - Make mockInitializer a var on Instantiable so the extension visitor can patch it with a simple mutation instead of rebuilding the struct Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Default-valued init parameters (e.g., `flag: Bool = false`) now bubble up to the root mock method as optional closure parameters. Users can override them at test time or let them fall back to the original default expression. Key changes: - Add createMockInitializerArgumentList to Initializer (includes all args) - Add defaultValueExpression to MockDeclaration for tracking defaults - collectMockDeclarations collects default-valued args from constant children - generatePropertyCode wraps default-arg bindings in scoped functions - generateMockRootCode handles root's own default-valued args - Default-valued params do NOT bubble through Instantiator boundaries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests for disambiguation, all instantiator boundaries, complex defaults, user-defined mock + defaults, multi-level bubbling, and grandchild stopped at instantiator boundary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers the unavailable-property nil pass-through and unexpected non-default argument error paths. Achieves 100% line coverage on both Initializer.swift and ScopeGenerator.swift. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expands the mock generation section of Manual.md to cover: - User-defined mock() methods being called by parent mocks - Macro validation requirements for user-defined mocks - Default-valued init parameters bubbling up to root mocks - Instantiator boundaries that stop bubbling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@escaping is only valid on function parameters, not in closure return type positions. When a default-valued init parameter has type @escaping () -> Void, the generated mock parameter would emit (SafeDIMockPath.X) -> @escaping () -> Void which is invalid Swift. Fix: add TypeDescription.strippingEscaping and use it when building MockDeclarations for default-valued arguments. Also fixes the enum name (was escapingVoid_to_Void, now Void_to_Void). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The compiler cannot always infer @Sendable/@mainactor attributes on closure literals from the ?? nil-coalescing context. Adding explicit type annotations (e.g., `let onComplete: @sendable () -> Void = ...`) ensures attributed closure types are preserved through the binding. Also adds tests for @mainactor and @sendable closure default parameters. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a child type has a user-defined static func mock() — even with no parameters — use .mock() for construction instead of falling back to the regular init. This prevents default-valued init params from incorrectly bubbling through types that handle their own test construction. Fixes @Sendable/@mainactor closure type mismatch in generated mocks for types like DelayedBackgroundTaskService. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ?? operator doesn't propagate type context (@mainactor, @sendable) to closure literals on the RHS. Using `if let x = override { x } else { defaultExpr }` gives each branch proper type inference from the explicit binding type annotation, so @MainActor/@sendable closure defaults compile correctly in the generated mock. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a user-defined mock() doesn't fulfill all dependencies, emit the same /* incorrectly configured */ comment used in production code gen. This triggers a build error directing the user to the @INSTANTIABLE macro fix-it, matching the production pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Default-valued parameter declarations from children were included in updatedCoveredLabels, causing received properties with the same label to be skipped from the uncovered list. This left root-level code referencing the raw closure parameter instead of a resolved value. Fix: exclude declarations with defaultValueExpression from the coverage check so received properties always get root-level bindings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add mockMethodMissingDependencyProducesDiagnostic macro test - Add mockMethodMissingMultipleDependencies macro test with fix-it - Add mockMethodWithPartialDeps test covering buildFixedParameterClause - Add defaultValuedParamDoesNotSuppressReceivedPropertyBinding test - Fix default-valued params suppressing received property bindings - Remove incorrect comment about macro tests being impossible - FixableInstantiableError.swift now at 100% coverage - InstantiableMacro.swift from 57 to 4 uncovered lines (dead code only) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The else branch for no-modifier mock functions was unreachable (mock detection requires static/class). The .inaccessibleInitializer switch case was handled by the isPublicOrOpen check above. Simplified both to eliminate uncoverable lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
generateMockCode now accepts currentModuleSourceFilePaths to filter out types from dependent modules. Previously, mocks were generated for ALL types in the tree (including dependent modules) and then discarded when they didn't match a manifest entry. Now they're skipped upfront, avoiding wasted scope tree construction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TypeDescription.strippingEscaping: test specifiers-only path (escaping removed, borrowing specifier preserved) - FixableInstantiableError: test descriptions and fix-it messages for mockMethodMissingArguments and mockMethodNotPublic Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SafeDI now automatically generates
mock()methods for@Instantiabletypes, with full dependency tree support includingInstantiator<T>,erasedToConcreteExistential, and@Forwardedproperties.Core mock generation
@Instantiabletype gets amock()static method that builds its full dependency subtree inline, with every node overridable via optional closure parametersSafeDIMockPathenums: Nested enums per dependency type describe where each dependency is created in the tree (case root,case childA, etc.)Instantiator<T>support: Mock wraps inline tree inInstantiator { forwarded in ... }closure. Forwarded properties become closure parameters. Transitive deps flow through from parent scope.erasedToConcreteExistentialsupport: Auto-wraps concrete types in erased wrappers (e.g.,AnyUserService(DefaultUserService.mock()))@SafeDIConfigurationproperties:generateMocks: BoolandmockConditionalCompilation: StaticString?@Instantiableparameter:mockAttributes: StaticStringfor global actor annotations (e.g.,@MainActor)SafeDIGeneratorplugin generates mocks for its own types. Mock generation defaults tofalsewhen no@SafeDIConfigurationexists.User-defined mock() methods
static func mock(...)in its@Instantiablebody, SafeDI skips generating a mock file for that typeChild.mock(...)instead ofChild(...), threading parameters through the user's custom method@Instantiablemacro validates that mock methods arepublic,static/class, and include parameters for all dependencies (with fix-its for missing parameters)mock()methods use.mock()for construction — default-valued init params do not bubble throughDefault-valued init parameters
flag: Bool = false,viewModel: VM? = nil) are exposed as optional closure parameters on the root mockInstantiator/SendableInstantiator/ErasedInstantiator/SendableErasedInstantiatorboundaries and at types with user-definedmock()methodsif let x = override { x } else { default }to preserve@MainActor/@Sendabletype context on closure defaults@escapingis stripped from default-valued closure types (invalid in return position); other attributes (@Sendable,@MainActor) are preservedmock()doesn't fulfill all dependencies, the generated code emits a/* @Instantiable type is incorrectly configured */comment (same pattern as production code gen), triggering a build error that directs users to the macro fix-itExample project updates
#Previewblocks updated to use.mock()with zero manual construction@SafeDIConfigurationfor per-module mock generation.#Previewblocks use.mock().Testing
==comparison (nocontainsassertions)Instantiator<T>with/without forwarded properties,erasedToConcreteExistential, extension-based types, deep nesting, multiple branches, protocol fulfillment, lazy self-instantiation cycles, default-valued arguments (Bool, String, Int, closures, Optional, complex expressions),@MainActor/@Sendableclosure defaults, user-defined mock methods (with/without params), disambiguation, instantiator boundaries, grandchild bubbling, misconfigured mock error comments, and moreTest plan
swift test)swift test --enable-code-coverage)swift build)xcrun xcodebuild)xcrun xcodebuild)swiftformat)🤖 Generated with Claude Code